import { URL, format as urlFormat } from 'url';
import { TextDecoder } from 'util';
import { pathOr } from 'ramda';
import format from 'date-fns/format';
import * as csv from '@fast-csv/parse';
import tld from 'tldjs';
import jsonPatch from 'fast-json-patch';
import fetch from 'fermionjs/lib/fetch';

import pkg from '../../../package.json';
import omitNested from '../helpers/omitNested';
import initServerInstance from '../initServerInstance';

export const ANY = '\x01*\x02';
const MERGE_THRESHOLD = {
	api: 8,
	static: 3,
	staticHtml: 8,
};

export default async function sitePortraitTask(server) {
	const {
		app: {
			model: { Balance, Job },
		},
	} = server;
	server.log.info('[网站画像]定时任务开始执行');

	const balanceMap = {};
	try {
		for await (const job of Job.find({
			cydEnabled: true,
			enableMonitor: true,
		})) {
			let balance;
			try {
				balance = await (balanceMap[job.uid]
					? Promise.resolve(balanceMap[job.uid])
					: Balance.findOne({ uid: job.uid }).lean());
				if (!balance) {
					throw new Error(
						`site portrait repeat task: not found balance uid(${job.uid})`
					);
				}
				balanceMap[job.uid] = balance;
			} catch (error) {
				server.log.error({ error }, '[网站画像]查询balance记录失败');
				continue;
			}
			if (
				balance.planExpireTime < Date.now() &&
				balance.balance.sitePortrait.total > 0
			) {
				server.log.error({ target: job.target.host }, '[网站画像]余额不足');
				continue;
			}

			await task({ server, job });
		}
	} catch (error) {
		server.log.error({ error }, '[网站画像]定时任务执行失败');
	}

	server.log(['info'], '[网站画像]定时任务执行完成');
}

export async function manualExecuteTask({ jobId, queryTimeout = 320000 }) {
	const server = await initServerInstance({
		loggerConfig: {
			name: 'systemRepeatTask',
			alwaysCreate: true,
		},
	});
	const job = await server.app.model.Job.findOne({ _id: jobId });
	return task({ server, job, queryTimeout });
}

export async function task({ server, job, queryTimeout }) {
	const {
		app: {
			config: {
				[pkg.name]: {
					sitePortraitSettings,
					sitePortraitRepeatTask: {
						limit,
						interval,
						dataSource: { api, tableName },
					},
				},
			},
			model: { SitePortrait, Task },
		},
		methods: { createOrUpdateTask },
	} = server;

	const now = new Date();
	const startTime = format(
		// 查询上次任务之后的日志
		new Date(now.getTime() - interval - Math.ceil(interval / 3)),
		'YYYY-MM-DD HH:mm:ss'
	);

	const target = job.target.hostname;
	server.log.info({ target }, '[网站画像]URL树构建');

	let responseCsv;
	try {
		responseCsv = await ckQuery({
			api,
			queryTimeout,
			sql:
				`SELECT DISTINCT req_method, host, req_uri, content_type ` +
				`FROM ${tableName} ` +
				`WHERE (start_time > '${startTime}') AND (host = '${target}') AND (resp_code >= 200) AND (resp_code < 300) ` +
				// 'ORDER BY host ASC, req_method ASC, req_uri ASC ' +
				`LIMIT ${limit}`,
		});
	} catch (error) {
		server.log.error({ target, error }, '[网站画像]查询URL失败');
		return;
	}

	let urlPatternTree;
	try {
		urlPatternTree = await buildUrlPatternTree({
			responseCsv,
			log: server.log,
		});
	} catch (error) {
		server.log.error({ target, error }, '[网站画像]生成模式树失败');
		return;
	}

	let sitePortrait;
	if (job.sitePortraitId) {
		try {
			sitePortrait = await SitePortrait.findOne({
				_id: job.sitePortraitId,
			});
		} catch (error) {
			server.log.error(
				{ error, sitePortraitId: job.sitePortraitId },
				'[网站画像]查询关联网站画像记录失败'
			);
			return;
		}
	}

	if (!sitePortrait) {
		server.log.debug({ target }, '新建网站画像');
		sitePortrait = new SitePortrait({
			target,
			securityStatus: 'warning',
			taskSettings: sitePortraitSettings.taskSettings,
			uid: job.uid,
			jobId: job._id,
			portrait: JSON.stringify(urlPatternTree),
			createTime: now,
			updateTime: now,
			version: 0,
		});
		job.sitePortraitId = sitePortrait._id;

		try {
			await Promise.all([sitePortrait.save(), job.save()]);
		} catch (error) {
			server.log.error({ target, error }, '[网站画像]更新失败');
			return;
		}

		return;
	}

	sitePortrait.updateTime = now;
	sitePortrait.version += 1;

	server.log.debug({ target }, '更新网站画像');
	const prevSitePortrait = JSON.parse(sitePortrait.portrait);
	const currentSitePortrait = mergeTree(
		JSON.parse(sitePortrait.portrait),
		urlPatternTree
	);
	treeCulling(currentSitePortrait);
	const patches = jsonPatch.compare(
		omitNested(currentSitePortrait, ['example', 'lastTime', 'firstTime']),
		omitNested(prevSitePortrait, ['example', 'lastTime', 'firstTime'])
	);
	if (patches.length === 0) {
		server.log.info({ target }, '[网站画像]：没有修改');
		return;
	}

	server.log.debug(
		{ target, patchesLength: patches.length },
		'[网站画像]URL树对比'
	);
	try {
		sitePortrait.nextExecDate = new Date(now.getTime() + interval);

		const task = new Task({
			domain: sitePortrait.target,
			target: sitePortrait.target,
			taskType: 'sitePortrait',
			result: {
				isChanged: true,
				addition: {
					version: sitePortrait.version,
					patches: JSON.stringify(patches),
				},
			},
			uid: job.uid,
			securityStatus: 'warning',
			refType: 'job',
			refId: sitePortrait.jobId,
			jobId: sitePortrait.jobId,
			status: 'completed',
			progress: 100,
			startTime: now,
			endTime: now,
			nextExecDate: sitePortrait.nextExecDate,
			triggerType: sitePortrait.triggerType,
			prevTaskId: sitePortrait.lastChangeId,
			taskSettings: sitePortrait.taskSettings,
		});
		sitePortrait.isChanged = true;
		sitePortrait.lastChangeId = task.id;
		sitePortrait.portrait = JSON.stringify(currentSitePortrait);

		await Promise.all([task.save(), sitePortrait.save()]);
	} catch (error) {
		server.log.error({ error }, '[网站画像]更新失败');
	}

	const changedUrls = getChangedApis({
		patches,
		urlTree: currentSitePortrait,
	});
	if (
		changedUrls.length &&
		job.enableMonitor &&
		job.verification.isVerified &&
		job.vulSettings.enable &&
		job.vulSettings.enableSitePortraitTriggerMonitor
	) {
		server.log.debug({ changedUrls }, '[网站画像]监测到API变化');
		createOrUpdateTask({
			taskType: 'vul',
			triggerType: 'manual',
			body: {
				taskSettings: {
					...job.vulSettings.toObject(),
					taskId: undefined,
					enableSitePortraitTriggerMonitor: true,
					collect: {
						...job.vulSettings.collect,
						includeUrl: changedUrls.map(x => encodeURI(decodeURI(x.trim()))),
					},
				},
				jobId: job._id,
				enableMonitor: true,
				immediateExec: true,
			},
		}).catch(error => {
			server.log.error({ error }, '[网站画像]下发漏洞扫描任务失败.');
		});
	}
}

/**
 * Clickhouse 查询
 * @param {{ sync: boolean, readOnly: boolean, responseParse: boolean }} [opts={ sync: false, readOnly: true, responseParse: true }] - 执行选项
 * @return {Promise<ReadableStream|string[]>}
 */
export function ckQuery({
	api,
	sql,
	readOnly = true,
	sync = false,
	responseParse = true,
	queryTimeout = 180 * 1000,
}) {
	const url = new URL(api);
	readOnly && url.searchParams.set('query', sql);

	return fetch(url, {
		method: readOnly ? 'GET' : 'POST',
		headers: { 'content-type': 'text/plain' },
		body: readOnly ? undefined : sql,
		responseType: sync ? 'text' : 'raw',
		timeout: queryTimeout,
	})
		.then(response => {
			if (!responseParse) {
				return sync ? response : response.body;
			}

			if (sync) {
				return new Promise((resolve, reject) => {
					const ret = [];
					csv
						.parseString(response, {
							headers: false,
							delimiter: '\t',
						})
						.on('error', error => reject(error))
						.on('data', row => {
							ret.push(row.map(x => (x === '\\N' ? null : x)));
						})
						.on('end', () => resolve(ret));
				});
			}

			return response.body.pipe(
				csv.parse({
					headers: false,
					delimiter: '\t',
				})
			);
		})
		.catch(error => {
			error.sql = sql;
			return Promise.reject(error);
		});
}

export const urlTokenize = (() => {
	const gb18030Decoder = new TextDecoder('gb18030');

	return urlStr => {
		const url = new URL(urlStr);

		const { domain, subdomain } = tld.parse(urlStr);
		url.domain = domain;
		url.subDomainMap = new Map();
		const subDomainArr = subdomain.split('.').filter(Boolean);

		for (let sdIdx = 0, sdLen = subDomainArr.length; sdIdx < sdLen; sdIdx++) {
			url.subDomainMap.set(`subDomain_${sdIdx}`, subDomainArr[sdIdx]);
		}

		const pathArr = (url.pathname || '')
			.split('/')
			.filter(Boolean)
			.map(urlPath => {
				try {
					return decodeURIComponent(urlPath);
				} catch (error) {
					try {
						const str = unescape(urlPath);
						return gb18030Decoder.decode(str2ab(str));
					} catch (e) {
						return urlPath;
					}
				}
			});
		url.pathArr = pathArr;

		if (pathArr.length && pathArr[pathArr.length - 1].indexOf(';') > 0) {
			const [before, ...after] = pathArr[pathArr.length - 1].split(';');
			pathArr[pathArr.length - 1] = before;
			url.urlCookieMap = new URLSearchParams(after.join(';'));
		}

		// 文件后缀
		if (pathArr.length >= 1 && pathArr[pathArr.length - 1]) {
			const parts = pathArr[pathArr.length - 1].split('.');
			if (parts.length > 1) {
				url.fileExtension = parts[parts.length - 1].toLowerCase();
			}
		}

		url.pathMap = pathArr.reduce((map, x, i) => {
			map.set(`path_${i}`, x);
			return map;
		}, new Map());

		url.toJSON = () => urlFormat(url);
		url.toString = url.toJSON;

		return url;
	};
})();

export async function buildUrlPatternTree({ responseCsv, log }) {
	const tree = {};
	let rowCount = 0;
	let rows = [];
	for await (const row of responseCsv) {
		rowCount += 1;

		const [method, host, url, contentType] = row;
		if (!url) {
			log.debug({ row }, 'urlPatternDetection URL 为空');
			continue;
		}

		const urlObj = urlTokenize(
			`http://${host}${url.replace(/\/\//g, '/').replace(/\/$/g, '')}`
		);
		urlObj.method = method.toLowerCase();
		urlObj.contentType = contentType.toLowerCase();
		urlObj.isStaticFileReq = isStaticFileReq(urlObj);
		rows.push(urlObj);

		if (rows.length >= 1000) {
			log.debug({ rows: rows.length }, 'urlPatternDetection mergeTree');
			const urlTree = skipDomain(buildReqUrlTree(rows));
			mergeTree(tree, urlTree);
			rows = [];
		}
	}

	if (rows.length >= 1) {
		const urlTree = skipDomain(buildReqUrlTree(rows));
		mergeTree(tree, urlTree);
		rows = [];
	}

	log.debug({ rowCount }, 'urlPatternDetection 执行完成.');
	treeCulling(tree);

	return tree;
}

export function str2ab(str, View = Uint8Array) {
	const buf = new ArrayBuffer(str.length);
	const bufView = new View(buf);
	for (let i = 0, strLen = str.length; i < strLen; i++) {
		bufView[i] = str.charCodeAt(i);
	}
	return buf;
}

export function buildReqUrlTree(reqUrls) {
	const tree = {};
	const nowTS = Date.now();

	for (let urlidx = 0; urlidx < reqUrls.length; urlidx++) {
		const url = reqUrls[urlidx];
		const urlType = url.isStaticFileReq ? 'static' : 'api';

		if (!tree[url.domain]) {
			tree[url.domain] = {
				type: 'domain',
				key: 'domain',
				value: url.domain,
				firstTime: nowTS,
				lastTime: nowTS,
				children: {},
				urlTypeMap: {},
			};
		}

		tree[url.domain].urlTypeMap[urlType] = true;
		let lastPath = tree[url.domain].children;

		for (const sub of url.subDomainMap) {
			const key = sub[0];
			const value = sub[1];

			if (!lastPath[value]) {
				lastPath[value] = {
					key,
					value,
					type: 'subDomain',
					firstTime: nowTS,
					lastTime: nowTS,
					children: {},
					urlTypeMap: {},
					patternMap: {},
				};
			}

			lastPath[value].urlTypeMap[urlType] = true;
			lastPath[value].patternMap[patternBuilder(value)] = true;
			lastPath = lastPath[value];
		}

		for (const i of url.pathMap) {
			const key = i[0];
			const value = i[1];

			if (!lastPath.children) {
				lastPath.children = {};
			}

			if (!lastPath.children[value]) {
				lastPath.children[value] = {
					key,
					value,
					type: 'path',
					firstTime: nowTS,
					lastTime: nowTS,
					urlTypeMap: {},
					patternMap: {},
				};
			}

			lastPath.children[value].urlTypeMap[urlType] = true;
			lastPath.children[value].patternMap[patternBuilder(value)] = true;
			lastPath = lastPath.children[value];
		}
		lastPath.isLeaf = true;
		lastPath.contentTypeMap = { [url.contentType.toLowerCase()]: true };
		lastPath.example = [url.toString()];

		if (!Object.keys(lastPath.children || {}).length) delete lastPath.children;

		for (const i of url.searchParams) {
			const key = i[0];
			const value = i[1];
			lastPath.query = lastPath.query || {};

			if (!lastPath.query[key]) {
				lastPath.query[key] = {
					value,
					firstTime: nowTS,
					lastTime: nowTS,
					patternMap: { [patternBuilder(key)]: true },
					example: [url.toString()],
				};
			} else if (lastPath.query[key].value !== value) {
				lastPath.query[key].value = ANY;
			}
		}

		if (!lastPath.methods) {
			lastPath.methods = {};
		}
		lastPath.methods[url.method] = true;
	}

	return tree;
}

export const isStaticFileReq = (() => {
	const apiFrameworkFileExtension = {
		php: true,
		jsp: true,
		asp: true,
		aspx: true,
		do: true,
	};
	const apiMethodMap = new Map([
		['post', true],
		['put', true],
		['delete', true],
	]);

	return urlObj => {
		if (urlObj.pathArr.length === 0) {
			return true;
		}

		if (apiMethodMap.has(urlObj.method)) return false;

		if (apiFrameworkFileExtension[urlObj.fileExtension]) return false;

		if (urlObj.pathname.indexOf('api/') > 0) return false;

		if (
			!urlObj.fileExtension &&
			Array.from(urlObj.searchParams.keys()).length > 0
		) {
			return false;
		}

		return true;
	};
})();

export function mergeTree(...args) {
	const [first, ...rest] = args.filter(Boolean);
	const queue = rest.reduce((arr, next) => {
		arr.push([first, next]);
		return arr;
	}, []);

	while (queue.length > 0) {
		const [a, b] = queue.shift();

		let aChild = a;
		let bChild = b;

		if (a.type && a.value && a.type === b.type) {
			if (a.value !== b.value) {
				a.value = ANY;
			}
			if (b.urlTypeMap) {
				a.urlTypeMap = Object.assign(a.urlTypeMap || {}, b.urlTypeMap);
			}
			if (b.contentTypeMap) {
				a.contentTypeMap = Object.assign(
					a.contentTypeMap || {},
					b.contentTypeMap
				);
			}
			if (b.example) {
				a.example = (b.example || [])
					.concat(a.example || [])
					.concat()
					.slice(0, 6);
				a.example = Array.from(new Set(a.example));
			}
			if (b.patternMap) {
				a.patternMap = a.patternMap || {};
				Object.keys(b.patternMap).forEach(key => {
					a.patternMap[key] = true;
				});
				a.patternMap = combineSimilarRegexPattern(a.patternMap);
			}
			if (b.firstTime) {
				a.firstTime = a.firstTime || b.firstTime;
				if (a.firstTime > b.firstTime) {
					a.firstTime = b.firstTime;
				}
			}
			if (b.lastTime) {
				a.lastTime = a.lastTime || b.lastTime;
				if (new Date(a.lastTime).getTime() < new Date(b.lastTime).getTime()) {
					a.lastTime = b.lastTime;
				}
			}

			if (b.children) {
				if (!a.children) a.children = {};
				aChild = a.children || {};
				bChild = b.children;
			} else if (b.query) {
				if (!a.query) a.query = {};
				aChild = a.query || {};
				bChild = b.query;
			} else {
				aChild = null;
				bChild = null;
			}
		}

		if (bChild && typeof bChild === 'object') {
			const bKeys = Object.keys(bChild);
			for (let i = bKeys.length - 1; i >= 0; i--) {
				let key = bKeys[i];
				const val = bChild[key];

				if (!aChild[key]) {
					aChild[key] = val;
				} else if (typeof val === 'object') {
					if (aChild[key].value !== val.value) {
						const aVal = aChild[key];
						aVal.value = ANY;
						delete aChild[key];
						aChild[ANY] = aVal;
						key = ANY;
					}
					queue.unshift([aChild[key], val]);
				}
			}
		}
	}

	return first;
}

export function treeCulling(tree) {
	const queue = [tree.children];
	while (queue.length > 0) {
		const subTrees = queue.shift() || {};

		const treeKeys = Object.keys(subTrees);
		if (treeKeys.length <= 1) {
			const tree = subTrees[treeKeys[0]] || {};
			if (tree.children) queue.unshift(tree.children);
			continue;
		}

		let any = subTrees[ANY];
		const keyLenMap = treeKeys.reduce(
			(obj, key) => {
				if (key === ANY) return obj;

				const route = subTrees[key];
				if (route.isLeaf && !route.urlTypeMap.api) {
					if (Object.keys(route.contentTypeMap)[0].indexOf('html') > 0) {
						const key = patternBuilder(route.value);
						obj.staticHtml[key] = obj.staticHtml[key] || [];
						obj.staticHtml[key].push(route);
					} else {
						Object.keys(route.contentTypeMap).forEach(c => {
							obj.static[c] = obj.static[c] || [];
							obj.static[c].push(route);
						});
					}
				} else {
					const key = patternBuilder(route.value);
					obj.api[key] = obj.api[key] || [];
					obj.api[key].push(route);
				}

				return obj;
			},
			{ static: {}, api: {}, staticHtml: {} }
		);
		// pattern类别大于阈值，移除长度
		if (
			Object.keys(keyLenMap.staticHtml).length >=
			MERGE_THRESHOLD.staticHtml * 2
		) {
			keyLenMap.staticHtml = Object.entries(keyLenMap.staticHtml).reduce(
				(obj, [pattern, routes]) => {
					const key = stripPatternLength(pattern);
					obj[key] = obj[key] || [];
					obj[key].push(...routes);
					return obj;
				},
				{}
			);
		}

		// 有分隔符的ID个数超过阈值
		if (
			Object.keys(keyLenMap.api).filter(p => p.split('\\p').length > 3)
				.length >=
			MERGE_THRESHOLD.api * 3
		) {
			keyLenMap.api = Object.entries(keyLenMap.api).reduce(
				(obj, [p, routes]) => {
					const key = p.split('\\p').length > 3 ? stripPatternLength(p) : p;
					obj[key] = obj[key] || [];
					obj[key].push(...routes);
					return obj;
				},
				{}
			);
		}
		Object.entries(MERGE_THRESHOLD).forEach(([type, threshold]) => {
			Object.values(keyLenMap[type]).forEach(routes => {
				if (
					(any &&
						(type !== 'static'
							? routes.every(
									x =>
										Object.keys(x.patternMap || {})[0] in
											(any.patternMap || {}) ||
										Object.keys(any.patternMap || {}).some(p =>
											new RegExp(p, 'u').test(x.value)
										)
							  )
							: routes.every(
									x => Object.keys(x.contentTypeMap)[0] in any.contentTypeMap
							  ))) ||
					routes.length >= threshold
				) {
					routes.forEach(x => {
						delete subTrees[x.value];
					});

					if (!any) {
						any = mergeTree.apply(null, routes);
					} else {
						any = mergeTree.apply(null, [any].concat(routes));
					}

					any.value = ANY;
					subTrees[ANY] = any;

					if (any.children) {
						queue.unshift(any.children);
					}
				} else {
					routes.forEach(x => {
						if (x.children) queue.unshift(x.children);
					});
				}
			});
		});
	}

	return tree;
}

/**
 * 目前不会有多个下级子域名的情况，所以把 path 移到顶部
 * @param {{}} tree
 * @returns {{}}
 */
export function skipDomain(tree) {
	let ret = Object.values(tree)[0] || {};

	let next = ret.children && Object.values(ret.children)[0];
	while (next && next.type !== 'path') {
		ret = next;
		next = ret.children && Object.values(ret.children)[0];
	}

	return ret;
}

export function getChangedApis({ urlTree, patches }) {
	const urls = patches.reduce((set, { path }) => {
		let fieldPath = path
			.slice(1)
			.split('/')
			.map(x => jsonPatch.unescapePathComponent(x));

		let queryIdx = 0;
		for (let i = fieldPath.length - 2; i >= 0; i -= 2) {
			if (fieldPath[i] !== 'query') continue;
			queryIdx = i;
		}
		if (queryIdx) {
			fieldPath = fieldPath.slice(0, queryIdx);
		}

		const node = pathOr({}, fieldPath, urlTree);
		const example = pathOr('', ['example', 0], node);
		if (
			(queryIdx > 0 ||
				node?.urlTypeMap?.api ||
				node?.contentTypeMap?.['text/html']) &&
			example
		) {
			set.add(example);
		}

		return set;
	}, new Set());

	return Array.from(urls);
}

export function stripPatternLength(pattern) {
	// 移除长度 \p{L}{2,3} => \p{L}
	return pattern.replace(
		/(\[(?:\\p{L}|\\p{N}|\\p{N}\\p{L})\])(?:{\d+(?:,\d+)?})/g,
		'$1'
	);
}

export function combineSimilarRegexPattern(patternMap) {
	const patternGroup = Object.keys(patternMap).reduce((obj, pattern) => {
		// 移除长度 \p{L}{2,3} => \p{L}
		const key = stripPatternLength(pattern);
		obj[key] = obj[key] || [];
		obj[key].push(pattern);
		return obj;
	}, {});

	return Object.values(patternGroup).reduce((obj, patterns) => {
		if (patterns.length === 1) {
			const p = patterns[0];
			obj[p] = true;
			return obj;
		}

		const patternLength = patterns.map(p =>
			Array.from(
				p.matchAll(
					// pattern                            长度, {3,5}           分割字符, 如果包含[匹配会出问题
					/(\[(?:\\p{L}|\\p{N}|\\p{N}\\p{L})\])(?:{(\d+)(?:,(\d+))?})(?:([^:\p{N}\p{L}[]+))?/g
				)
			).map(x => ({
				p: x[1],
				start: parseInt(x[2], 10),
				end: parseInt(x[3] || x[2], 10),
				delimiter: x[4],
			}))
		);

		const p = patternLength[0].reduce((patternStr, item, idx) => {
			const { p, start, end } = patternLength.reduce(
				(obj, pattern) => {
					const { start, end } = pattern[idx];

					if (obj.start > end) {
						obj.start = end;
					}
					if (obj.end < start) {
						obj.end = start;
					}
					if (obj.start > start) {
						obj.start = start;
					}
					if (obj.end < end) {
						obj.end = end;
					}
					return obj;
				},
				{
					p: item.p,
					start: item.start,
					end: item.end,
				}
			);

			let ret = p;
			if (start === end) {
				ret += `{${start}}`;
			} else {
				ret += `{${start},${end}}`;
			}

			return patternStr + ret + (item.delimiter || '');
		}, '');
		obj[p] = true;

		return obj;
	}, {});
}

export function regexBuilder(str = '') {
	let ret = '';
	if (/\p{N}/u.test(str)) {
		// number
		ret += '\\p{N}';
	}
	if (/\p{L}/u.test(str)) {
		// letter
		ret += '\\p{L}';
	}
	return `[${ret}]{${str.length}}`;
}

export function patternBuilder(str) {
	return str
		.replace(/([^:\p{N}\p{L}])/gu, escapeRegExp) // escape symbol
		.replace(/([\p{N}\p{L}]+)/giu, regexBuilder);
}

function escapeRegExp(string) {
	return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
